
//
// 120702 - AH - Created.
// 120721 - AH - Indexes by bisons and get(String, Bison).
// 120722 - AH - (Midtun) get(String, Bison...).
// 120730 - AH - (Kryddar) findInContext(String, Bison).
// 120807 - AH - (Midtun) Improved method names (away with stupid get methods)
//               and java doc.
// 121007 - AH - (Midtun) getSocratons(Bison).
// 121015 - AH - (Kryddar *) setState(Socraton, Bison, String).
// 121022 - AH - (Midtun) Made iterable over Socraton it contains.
//

package org.soekrat.engine.socraton;

import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.soekrat.Log;
import org.soekrat.engine.Matcher;

/**
 * <p>
 *   Where
 *   {@link Socraton}s live.
 * </p>
 * @author Arne Halvorsen (AH)
 * @author Laetitia Le Chatton (LLC)
 */
public final class Socratons implements Iterable<Socraton> 
{
  private Map<Object, Socraton> m_socById=new HashMap<Object, Socraton>();
  
  private Map<Bison, List<Socraton>> m_socByBisons=
    new HashMap<Bison, List<Socraton>>();
  
  public Socraton add(Socraton socraton, String ctx)
  {
    if (socraton==null)
    {
      throw new NullPointerException("socraton");
    }
    
    Log.log(c_log, this, "add", "Adds "+socraton);
    
    Socratons owner=socraton.getOwner();
    if (owner!=null && owner!=this)
    {
      throw new IllegalArgumentException("sorcaton: "+socraton+
        " blong to other pool");
    }
    
    socraton.setOwner(this);
    
    Object id=socraton.getId();
    
    Socraton stored=m_socById.get(id);
    
    // Same pointer, nothing to do.
    if (socraton==stored) return socraton;
    
    // Have no existing so stores the one received.
    if (stored==null)
    {
      if (ctx!=null)
      {
        socraton.setBison(ctx, new Bison(socraton.getId().toString()));
      }
      m_socById.put(id, socraton);
      updateSocByBisons(socraton);    
      return socraton;
    }
    
    return augment(stored, socraton);
  }
  
  private Socraton augment(Socraton stored, Socraton socraton)
  {
    List<String> ctxs=socraton.getContexts();
    
    for (String ctx : ctxs)
    {
      Bison[] bisons=socraton.getBisons(ctx);
      for (Bison bison : bisons) 
      {
        setState(stored, bison, ctx);
      }
    }
    
    return stored;
  }
  
  private List<Socraton> getFromsocByBisons(boolean log, Bison bison, 
    Matcher matcher)
  { 
    Log.log(log, this, "getFromsocByBisons", "bison: "+bison);
    if (matcher==null)
    {
      Log.log(log, this, "getFromsocByBisons", "  no matcher, normal lookup");
      return m_socByBisons.get(bison);
    }
    else
    {
      Log.log(log, this, "getFromsocByBisons", "  got matcher, scan");
      for (Map.Entry<Bison, List<Socraton>> curr : m_socByBisons.entrySet())
      {
        Bison key=curr.getKey();
        Log.log(log, this, "getFromsocByBisons", "    compare to: "+key);
        if (matcher.match(key.getLocation(), bison.getLocation())) 
        {
          Log.log(log, this, "getFromsocByBisons", "      matched");
          return curr.getValue();
        }
        else
        {
          Log.log(log, this, "getFromsocByBisons", "      did not match");
        }
      }
    }
    return null;
  }
    
  /**
   * <p>
   *   Sets state for given 
   *   {@link Socraton}.
   * </p>
   * @param socraton {@link Socraton}.
   * @param state    State.
   * @param ctx      Context name for state.
   * @throws IllegalArgumentException If {@code socraton} not found.
   */
  public void setState(Socraton socraton, Bison state, String ctx)
  {
    if (socraton==null)
    {
      throw new NullPointerException("socraton");
    }
    if (state==null)
    {
      throw new NullPointerException("state");
    }
    if (ctx==null)
    {
      throw new NullPointerException("ctx");
    }
    
    Socraton s=m_socById.get(socraton.getId());
    if (s==null)
    {
      throw new IllegalArgumentException("socraton not found");
    }
    if (s!=socraton)
    {
      throw new IllegalArgumentException("socraton not found");
    }
    
    socraton.setBison(ctx, state);
    List<Socraton> l=m_socByBisons.get(state);
    if (l==null)
    {
      l=new ArrayList<Socraton>();
      m_socByBisons.put(state, l);
    }
    
    if (!l.contains(socraton)) l.add(socraton);
  }
  
  // Specified in Iterable<Socraton>.
  public Iterator<Socraton> iterator(){ return m_socById.values().iterator(); }

  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public Socratons(){}
  
  /**
   * <p>
   *   Gets number of 
   *   {@link Socraton}s.
   * </p>
   * @return Count.
   */
  public int size(){ return m_socById.size(); }
  
  void updateSocByBisons(Socraton s)
  {
    List<Bison> bisons=s.getBisons();
    for (Bison bison : bisons)
    {
      updateStateIndex(s, bison);  
    }
  }
  
  void updateStateIndex(Socraton s, Bison state)
  {
    List<Socraton> socs=m_socByBisons.get(state);
    if (socs==null)
    {
      socs=new ArrayList<Socraton>();
      m_socByBisons.put(state, socs);
    }
    if (!socs.contains(s)) socs.add(s);
  }
  
  /**
   * <p>
   *   Gets a 
   *   {@link Socraton} with given id as state.
   *  
   *   If 
   *   {@link Socraton} does not exist creates id out of given state. It will
   *   not create if given state already exist for the 
   *   {@code Socraton}.  
   * </p>
   * <p>
   *   This means that returned 
   *   {@link Socraton} has been created in given context or now was.
   * </p>
   * @param context Context to get 
   *                {@link Socraton} in.
   * @param bison   Id of
   *                {@link Socraton} to also be a state in given context.                
   * @return {@link Socraton}.
   */
  public Socraton getOrCreateWithIdAsState(String context, Bison id)
  {
    return getOrCreateWithIdAsState(context, id, null);
  }
  
  /**
   * <p>
   *   Gets a 
   *   {@link Socraton} with given id as state.
   *  
   *   If 
   *   {@link Socraton} does not exist creates id out of given state. It will
   *   not create if given state already exist for the 
   *   {@code Socraton}.  
   * </p>
   * <p>
   *   This means that returned 
   *   {@link Socraton} has been created in given context or now was.
   * </p>
   * @param context Context to get 
   *                {@link Socraton} in.
   * @param bison   Id of
   *                {@link Socraton} to also be a state in given context.
   * @param matcher {@link Matcher} to check if a 
   *                {@link Bison} is the same as another 
   *                {@code Bison}. If {@code null} uses 
   *                {@link Bison#equals(Object)}.                
   * @return {@link Socraton}.
   */
  public Socraton getOrCreateWithIdAsState(String context, Bison id, 
    Matcher matcher)
  {
    if (context==null)
    {
      throw new NullPointerException("context");
    }
    if (id==null)
    {
      throw new NullPointerException("id");
    }
    
    //
    // TODO, maybe put in tests so invariant of Socraton can not exist with
    // the same state which is the id in more than one context.
    //
    
    String sid=id.getLocation();
    
    Socraton retVal=getOrCreateWithId(sid);
    if (!retVal.contains(context, id, matcher))
    {
      retVal.setBison(context, id);
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Gets an existing 
   *   {@link Socraton} with given id or if not exists create it 
   *   'out of context' with given id.
   * </p>
   * @param id Socraton's id.
   * @return {@link Socraton}.
   */
  public Socraton getOrCreateWithId(Object id)
  {
    if (id==null)
    {
      throw new NullPointerException("id");
    }
    
    Socraton retVal=m_socById.get(id);
    if (retVal==null)
    {
      retVal=new Socraton(id, this);
      m_socById.put(id, retVal);
    }
    return retVal;
  }
  
  /**
   * <p>
   *   Gets or creates
   *   {@link Socraton}s identified by given
   *   {@link Bison}s in a given context.
   * </p>
   * @param context Name on context.
   * @param bison   {@code Bison}s.
   * @return {@code Socraton}s.
   */
  public Socraton[] getOrCreateFromStates(String context, List<Bison> bisons)
  {
    return getOrCreateFromStates(context, bisons, null);
  }
  
  /**
   * <p>
   *   Gets or creates
   *   {@link Socraton}s identified by given
   *   {@link Bison}s in a given context.
   * </p>
   * @param context Name on context.
   * @param bison   {@code Bison}s.
   * @param matcher {@link Matcher} to check if a 
   *                {@link Bison} is the same as another 
   *                {@code Bison}. If {@code null} uses 
   *                {@link Bison#equals(Object)}.
   * @return {@code Socraton}s.
   */
  public Socraton[] getOrCreateFromStates(String context, List<Bison> bisons,
    Matcher matcher)
  {
    if (context==null)
    {
      throw new NullPointerException("context");
    }
    if (bisons==null)
    {
      throw new NullPointerException("bisons");
    }
    
    int n=bisons.size();
    
    Socraton[] retVal=new Socraton[n];
    
    for (int i=0; i<n; i++)
    {
      Bison bison=bisons.get(i);
      if (bison==null)
      {
        throw new NullPointerException("bisons["+i+"]");
      }
      
      retVal[i]=getOrCreateFromState(context, bison, matcher);
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Gets or creates
   *   {@link Socraton} identified by given
   *   {@link Bison} in a given context.
   * </p>
   * @param context Name on context.
   * @param bison   {@code Bison}. 
   * @return {@code Socraton}.
   */
  public Socraton getOrCreateFromState(String context, Bison bison)
  {
    return getOrCreateFromState(context, bison, null);
  }
  
  /**
   * <p>
   *   Gets or creates
   *   {@link Socraton} identified by given
   *   {@link Bison} in a given context.
   * </p>
   * @param context Name on context.
   * @param bison   {@code Bison}.
   * @param matcher {@link Matcher} to check if a 
   *                {@link Bison} is the same as another 
   *                {@code Bison}. If {@code null} uses 
   *                {@link Bison#equals(Object)}. 
   * @return {@code Socraton}.
   */
  public Socraton getOrCreateFromState(String context, Bison bison,
    Matcher matcher)
  {
    if (context==null)
    {
      throw new NullPointerException("context");
    }
    if (bison==null)
    {
      throw new NullPointerException("bison");
    }
    
    Socraton s=m_socById.get(bison.getLocation());
    if (s!=null)
    {
      if (s.contains(context, bison, matcher))
      {
        // Case 0 : Nothing to edit: found Socraton with bison as both id and
        // state.
        return s;
      }
      else
      {
        // Case 1 : Found Socraton with bison as id but need to update it with
        // bison as state.
        s.setBison(context, bison);
        return s;
      }
    }
  
    //List<Socraton> socratons=m_socByBisons.get(bison);
    List<Socraton> socratons=getFromsocByBisons(c_log, bison, matcher);
    if (socratons!=null)
    {
      for (Socraton socraton : socratons) 
      {
        if (socraton.contains(context, bison, matcher)) 
        {
          // Case 2 : Nothing to edit: Found Socraton with bison as state in 
          // this context, return it as is.
          return socraton;
        }
      }
    }
    
    // Case 3 : None found, create one in the given context: id and state (in
    // given context) is the bison.
    s=new Socraton(bison.getLocation(), this);
    s.setBison(context, bison);
    m_socById.put(bison.getLocation(), s);
    return s;
  }
  
  /**
   * <p>
   *   Gets 
   *   {@link Socraton}s that has given state.
   * </p>   
   * @param state State.
   * @return {@link Socraton}s.
   */
  public List<Socraton> getSocratons(Bison state)
  {
    if (state==null)
    {
      throw new NullPointerException("state");
    }
    
    List<Socraton> retVal=new ArrayList<Socraton>();
    
    List<Socraton> l=getFromsocByBisons(c_log, state, null);
    if (l!=null) retVal.addAll(l);
    
    return retVal;
  }
  
  /**
   * <p>
   *   Finds
   *   {@link Socraton}s that has given state in given context.
   * </p>
   * @param ctx   Context.
   * @param state State.
   * @return {@code Socraton}s found.
   */
  public Socraton[] findInContext(String ctx, Bison state)
  {
    return findInContext(ctx, state, null);
  }
  
  /**
   * <p>
   *   Finds
   *   {@link Socraton}s that has given state in given context.
   * </p>
   * @param ctx     Context.
   * @param state   State.
   * @param matcher {@link Matcher} to check if a 
   *                {@link Bison} is the same as another 
   *                {@code Bison}. If {@code null} uses 
   *                {@link Bison#equals(Object)}. 
   * @return {@code Socraton}s found.
   */
  public Socraton[] findInContext(String ctx, Bison state, Matcher matcher)
  {
    if (ctx==null)
    {
      throw new NullPointerException("ctx");
    }
    if (state==null)
    {
      throw new NullPointerException("state");
    }
    
    boolean log=c_log;
    
    Log.log(log, this, "findInContext", "  ctx: "+ctx+", state="+state);
    
    List<Socraton> l=getFromsocByBisons(log, state, matcher);
    
    Log.log(log, this, "findInContext", 
      "  found from getFromsocByBisons: "+(l!=null));
    
    if (l==null) return Socraton.EMPTY_ARRAY;
    
    List<Socraton> found=new ArrayList<Socraton>();
    
    for (Socraton socraton : l)
    {
      Log.log(log, this, "findInContext", "    socraton: "+socraton);
      if (socraton.contains(ctx, state, matcher)) 
      {
        Log.log(log, this, "findInContext", "    found");
        found.add(socraton);  
      }
      else
      {
        Log.log(log, this, "findInContext", "    not found");
      }
    }
    
    return found.toArray(Socraton.EMPTY_ARRAY);
  }
  
  private static boolean doLog(String ctx)
  {
    return "ContentWordOntology".equals(ctx);
  }
  
  /**
   * <p>
   *   Tells if there is at least one.
   *   {@link Socraton}s has given state in given context.
   * </p>
   * @param ctx   Context.
   * @param state State.
   * @return {@code true} if has, {@code false} if has not.
   */
  public boolean hasInContext(String ctx, Bison state, Matcher matcher)
  {
    if (ctx==null)
    {
      throw new NullPointerException("ctx");
    }
    if (state==null)
    {
      throw new NullPointerException("state");
    }
    
    List<Socraton> l=getFromsocByBisons(c_log, state, matcher);
    if (l==null) return false;
    
    for (Socraton socraton : l)
    {
      if (socraton.contains(ctx, state, matcher)) return true;  
    }
    
    return false;
  }
  
  @Override
  public String toString()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    for (Socraton socraton : m_socById.values())
    {
      pw.println(socraton);  
    }
    
    pw.flush();
    return caw.toString();
  }
  
  public String socratonByBisonToString()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    for (Map.Entry<Bison, List<Socraton>> curr : m_socByBisons.entrySet())
    {
      pw.println("state: "+curr.getKey());
      pw.println("  "+curr.getValue());
    }
    
    pw.flush();
    return caw.toString();
  }
  
  public static void main(String[] args)
  {
    Socratons socratons=new Socratons();
    
    Socraton socraton=new Socraton("Arne");
    socraton.addBison("Test", new Bison("Bjarne"));
    socratons.add(socraton, "Test");
    
    Socraton socraton2=new Socraton("Arne");
    socraton2.addBison("Test", new Bison("Eva"));
    socratons.add(socraton2, "Test");
    
    System.out.println(socratons.socratonByBisonToString());
  }
  
  /**
   * <p>
   * </p>
   * @param args
   */
  /*
  public static void main(String[] args)
  {
    Socratons socratons=new Socratons();
    
    Socraton arne=socratons.getOrCreateWithId("Arne");
    System.out.println(arne);
    
    Socraton laetitia=socratons.getOrCreateWithId("Laetitia");
    laetitia.setBison("synonyms", 
      new Bison("kitty cat, plato, silly french girl"));
    System.out.println(laetitia);
    
    System.out.println("are arne and laetitia the same: "+arne.same(laetitia));
    
    Socraton arne2=socratons.getOrCreateWithId("Arne");
    System.out.println("are arne and arne2 the same: "+arne.same(arne2));
    
    Socraton laetitia2=socratons.getOrCreateWithId("Laetitia");
    laetitia2.setBison("synonyms", new Bison("hylleblomst"));
    System.out.println(laetitia);
  }
  */
  
  private static final boolean c_log=true;
  
}
